#include "scanner.h"
#include <QDirIterator>
#include <QtCore>
#include "imageloader.h"
#include "mediaitemimpl.h"
#include <libraw/libraw.h>
#include <xmp.h>

#define NS_XAP  "http://ns.adobe.com/xap/1.0/"
#define NS_DC   "http://purl.org/dc/elements/1.1/"
#define NS_TIFF "http://ns.adobe.com/tiff/1.0/"

QSet<QString> Scanner::s_rawformats =
        QSet<QString>::fromList(QStringList()
                                << "dng" << "crw" << "cr2" << "raw" << "erf" << "raf" << "3fr" << "dcr" << "dcs" << "kdc" << "raw" << "dng"
                                << "rwl" << "mef" << "mrw" << "mdc" << "nef" << "nrw" << "orf" << "raw" << "rw2" << "pef" << "dng" << "x3f"
                                << "cs1" << "cs4" << "cs16" << "srf" << "sr2" << "arw");

Scanner::Scanner()
    : m_stop(false)
{
    start();
}

Scanner::~Scanner()
{
    m_mutex.lock();
    m_stop = true;
    m_cond.wakeAll();
    m_mutex.unlock();
    wait();
}

void Scanner::scan(Database *database, const QString & path)
{
    m_mutex.lock();
    QueueItem qi;
    qi.path = path;
    QList<MediaItemPtr> all = database->all();
    foreach(MediaItemPtr item, all)
        qi.data[item->metadata().filename] = qMakePair(item, item->impl()->metadata());
    m_queue.append(qi);
    m_cond.wakeAll();
    m_mutex.unlock();
}

bool Scanner::update(MediaItemImpl::Metadata &metadata)
{
    QFileInfo fi(metadata.filename);
    QDateTime mtime = fi.lastModified();
    if(metadata.modified > mtime.addSecs(2) || metadata.modified.addSecs(2) < mtime) {
        metadata.modified = fi.lastModified();
        metadata.size     = fi.size();
        metadata.isRaw = s_rawformats.contains(fi.suffix().toLower());
        if(metadata.isRaw)
            loadRawMetadata(metadata);
        else
            loadXmpMetadata(metadata);
        return true;
    }
    else
        return false;
}

void Scanner::loadRawMetadata(MediaItemImpl::Metadata &metadata)
{
    libraw_data_t * lr = libraw_init(0);
    if(lr) {
        int r = libraw_open_file(lr, metadata.filename.toStdString().c_str());
        if(r == 0) {
            metadata.created.setTime_t(lr->other.timestamp);
            if(lr->other.desc != NULL)
                metadata.keywords = QString(lr->other.desc).split(QChar(' ')).toSet();
        }
        libraw_close(lr);
    }
    if(QFile::exists(metadata.filename + ".xmp")) {
        xmp_file_t * xmp = xmp_file_open(metadata.filename.toStdString().c_str());
        if(xmp) {
            // read keywords
            metadata.keywords.clear();
            int count = xmp_file_get_array_count(xmp, NS_DC, "dc:subject");
            for(int i = 0; i < count; i++) {
                const char * str = xmp_file_get_array_item(xmp, NS_DC, "dc:subject", i);
                if(str)
                    metadata.keywords << str;
            }
            // destroy xmp object
            xmp_file_close(xmp);
        }
    }
}

void Scanner::loadXmpMetadata(MediaItemImpl::Metadata &metadata)
{
    xmp_file_t * xmp = xmp_file_open(metadata.filename.toStdString().c_str());
    if(xmp) {
        // read creation date
        const char * date = xmp_file_get_property(xmp, NS_XAP, "xmp:CreateDate");
        if(date != NULL)
            metadata.created = QDateTime::fromString(date, Qt::ISODate);

        // read keywords
        metadata.keywords.clear();
        int count = xmp_file_get_array_count(xmp, NS_DC, "dc:subject");
        for(int i = 0; i < count; i++) {
            const char * str = xmp_file_get_array_item(xmp, NS_DC, "dc:subject", i);
            if(str)
                metadata.keywords << str;
        }

        /*// read orientation
        const char * str = xmp_file_get_property(xmp, NS_TIFF, "Orientation");
        if(str != NULL) {
            switch(atoi(str)) {
            case 1: m_orientation = 0; break;
            case 6: m_orientation = 1; break;
            case 3: m_orientation = 2; break;
            case 8: m_orientation = 3; break;
            }
        }*/

        // destroy xmp object
        xmp_file_close(xmp);
    }
}

void Scanner::run()
{
    m_mutex.lock();
    while(!m_stop) {
        if(!m_queue.empty()) {
            QueueItem qi = m_queue.takeFirst();
            m_mutex.unlock();
            scan(qi);
            m_mutex.lock();
            continue;
        }
        m_cond.wait(&m_mutex);
    }
    m_mutex.unlock();
}

void Scanner::collect(ImageLoader & loader, const QString & path, QSet<QString> & files)
{
    QDir dir(path);
    QFileInfoList entries = dir.entryInfoList();
    foreach(QFileInfo fi, entries) {
        if(!fi.fileName().startsWith('.')) {
            if(fi.isDir()) {
                if(fi.fileName() != "$RECYCLE.BIN")
                    collect(loader, fi.filePath(), files);
            }
            else if(fi.isFile()) {
                if(loader.supports(fi.suffix().toLower()))
                    files.insert(fi.filePath());
            }
        }
    }
}

void Scanner::scan(const QueueItem & qi)
{
    emit status("scanning " + qi.path + "...");

    // scan folder for supported files
    ImageLoader loader;
    QSet<QString> files;
    collect(loader, qi.path, files);
    int n = files.size();
/*    QDirIterator it(qi.path, QDirIterator::Subdirectories);
    while(it.hasNext()) {
        QString filename = it.next();
        if(it.fileInfo().isFile() && loader.supports(it.fileInfo().suffix().toLower()))
            files.insert(filename);
        QMutexLocker ml(&m_mutex);
        if(m_stop)
            return;
    }*/

    // remove files from database that are below that path but were not found
    QList<QPair<MediaItemPtr, MediaItemImpl::Metadata> > updates;
    for(QMap<QString, QPair<MediaItemPtr, MediaItemImpl::Metadata> >::iterator it = qi.data.begin(); it != qi.data.end(); it++) {
        if(it->second.filename.startsWith(qi.path)) {
            if(files.contains(it->second.filename))
                updates.append(*it);
            else {
                emit remove(it->first);
                QString str = it->second.filename;
                qDebug(str.toAscii());
            }
        }
        else {
            emit remove(it->first);
            QString str = it->second.filename;
            qDebug(str.toAscii());
        }
        QMutexLocker ml(&m_mutex);
        if(m_stop)
            return;
    }

    emit status("update modified items...");

    // update existing items
    for(QList<QPair<MediaItemPtr, MediaItemImpl::Metadata> >::iterator it = updates.begin(); it != updates.end(); it++) {
        if(update(it->second)) {
            emit update(it->first, it->second);
        }
        files.remove(it->second.filename);
        QMutexLocker ml(&m_mutex);
        if(m_stop)
            return;
    }

    // add new items
    int i = n - files.size();
    foreach(QString file, files) {
        MediaItemImpl::Metadata metadata;
        metadata.filename = file;
        metadata.collection = Database::collection(qi.path, file);
        update(metadata);
        emit add(MediaItemPtr(new MediaItemImpl(metadata)));
        i++;
        if(i % 10 == 0) {
            emit status(QString("add new items... (%1%)").arg(i * 100 / n));
            QMutexLocker ml(&m_mutex);
            if(m_stop)
                return;
        }
    }

    emit status("scan complete");
}
